home *** CD-ROM | disk | FTP | other *** search
/ Delphi Magazine Collection 2001 / Delphi Magazine Collection 20001 (2001).iso / DISKS / Issue56 / Splat / shapes.pas < prev    next >
Encoding:
Pascal/Delphi Source File  |  2000-01-28  |  15.3 KB  |  559 lines

  1. unit Shapes;
  2.  
  3. // Splat.
  4. // Shapes and the shape list.
  5. // To add new shapes, inherit from TShape, and call RegisterShapes.
  6. //
  7. // Copyright ⌐ 2000 Tempest Software, Inc.
  8.  
  9. interface
  10.  
  11. uses Windows, SysUtils, Classes, Forms, Graphics, Types, Contnrs;
  12.  
  13. type
  14.   TShape = class;
  15.   TShapeClass = class of TShape;
  16.  
  17.   // Maintain a list of TShape-descendent objects.
  18.   TShapeList = class(TObjectList)
  19.   private
  20.     fHeight, fWidth: NaturalInt;  // screen size
  21.     function GetShape(Index: NaturalInt): TShape;
  22.   public
  23.     class function AnyShapeClass: TShapeClass;
  24.     constructor Create(Width: NaturalInt = 0; Height: NaturalInt = 0);
  25.  
  26.     // Create a random shape and add it to the list.
  27.     procedure AddShape(X, Y: NaturalInt);
  28.  
  29.     // Add the Help text to the center of the screen.
  30.     procedure AddHelp;
  31.  
  32.     // Draw all the shapes on the canvas.
  33.     procedure Draw(Canvas: TCanvas);
  34.  
  35.     // Iterate the next generation of shapes.
  36.     procedure NextGeneration;
  37.  
  38.     // Get the shapes in the list.
  39.     property Shapes[Index: NaturalInt]: TShape read GetShape; default;
  40.  
  41.     property Height: NaturalInt read fHeight;
  42.     property Width: NaturalInt read fWidth;
  43.   end;
  44.  
  45.   // Abstract base class for all shapes. Maintain the position of the
  46.   // shape's center, size, and color. Each generation, fade the color
  47.   // and move the shape. When the color becomes black, or when the shape
  48.   // moves off the screen, the list deletes it.
  49.   TShape = class
  50.   private
  51.     fColor: TColor;             // Color of shape
  52.     fDelta: TPoint;             // Position change for each generation
  53.     fPosition: TPoint;          // Center of shape
  54.     fSize: TSize;
  55.   protected
  56.     constructor Create(Position: TPoint); virtual;
  57.     // Randomly change the shape's color. The default is to fade towards black.
  58.     procedure ChangeColor; virtual;
  59.     // Draw this shape on the canvas, at the current position,
  60.     // using the current color. Derived classes must override this method.
  61.     procedure Draw(Canvas: TCanvas); virtual; abstract;
  62.     // Randomly change the size of the shape.
  63.     procedure ChangeSize; virtual;
  64.     // Generate a random position Delta, which can be positive or negative.
  65.     procedure GenerateDelta; virtual;
  66.  
  67.     // Get the shape's bounding box.
  68.     procedure BoundingBox(var Rect: TRect); virtual;
  69.     function GetBottom: Integer; virtual;
  70.     function GetLeft: Integer; virtual;
  71.     function GetRight: Integer; virtual;
  72.     function GetTop: Integer; virtual;
  73.  
  74.     // Return True if the color is not black and if the shape's bounding
  75.     // box is still visible on the screen.
  76.     function IsVisible(Width, Height: NaturalInt): Boolean; virtual;
  77.     // Move the shape's position by its delta.
  78.     procedure Move; virtual;
  79.  
  80.     // Generate the next generation by fading the color.
  81.     procedure NextGeneration(Width, Height: NaturalInt); virtual;
  82.  
  83.   public
  84.     property Color: TColor read fColor write fColor;
  85.     property Delta: TPoint read fDelta write fDelta;
  86.     property Position: TPoint read fPosition write fPosition;
  87.     property XPosition: Integer read fPosition.X write fPosition.X;
  88.     property YPosition: Integer read fPosition.Y write fPosition.Y;
  89.     property Size: TSize read fSize write fSize;
  90.     property XSize: Integer read fSize.CX write fSize.CX;
  91.     property YSize: Integer read fSize.CY write fSize.CY;
  92.  
  93.     property Left: Integer read GetLeft;
  94.     property Right: Integer read GetRight;
  95.     property Top: Integer read GetTop;
  96.     property Bottom: Integer read GetBottom;
  97.   end;
  98.  
  99.   // The THelp shape tells the user how to end the program.
  100.   // The TShapeList always starts with a THelp object in the list.
  101.   // The help message gets smaller with each generation.
  102.   THelp = class(TShape)
  103.   private
  104.     fFontHeight: Single;
  105.   protected
  106.     function IsVisible(Width, Height: NaturalInt): Boolean; override;
  107.   public
  108.     constructor Create(Position: TPoint); override;
  109.     procedure Draw(Canvas: TCanvas); override;
  110.     procedure ChangeSize; override;
  111.     procedure Move; override;
  112.     property FontHeight: Single read fFontHeight write fFontHeight;
  113.   end;
  114.  
  115.   // Elliptical shape.
  116.   TEllipse = class(TShape)
  117.   public
  118.     procedure Draw(Canvas: TCanvas); override;
  119.   end;
  120.  
  121.   // Regular polygon with 3-12 vertices. The polygon starts small
  122.   // and grows with each generation.
  123.   TPolygon = class(TShape)
  124.   private
  125.     fNumVertices: PositiveInt;
  126.   public
  127.     constructor Create(Position: TPoint); override;
  128.     procedure Draw(Canvas: TCanvas); override;
  129.     property NumVertices: PositiveInt read fNumVertices;
  130.   end;
  131.  
  132.   // Make sure there are plenty of small, simple shapes, so
  133.   // create distinct classes for triangles and rectangles.
  134.   // Keep life simple by creating only equilateral triangles.
  135.   TTriangle = class(TPolygon)
  136.   public
  137.     procedure AfterConstruction; override;
  138.   end;
  139.   // Use a distinct rectangle class so we can have shapes other
  140.   // than squares (especially since squares are drawn so they
  141.   // look like lozenges).
  142.   TRectangle = class(TShape)
  143.   public
  144.     constructor Create(Position: TPoint); override;
  145.     procedure Draw(Canvas: TCanvas); override;
  146.   end;
  147.  
  148.   // Regular star. Just like a polygon, but with extra vertices
  149.   // interpolated between the usual vertices. The extra vertices
  150.   // are at a radius of 1/2 the polygon's radius. The result is
  151.   // a regular star.
  152.   TStar = class(TPolygon)
  153.   public
  154.     procedure Draw(Canvas: TCanvas); override;
  155.     constructor Create(Position: TPoint); override;
  156.   end;
  157.  
  158. procedure RegisterShapes(Shapes: array of TShapeClass);
  159.  
  160. const
  161.   DeltaColor = 3;
  162.   DeltaDimension = 3;
  163.   InitialDimension = 10;
  164.   DeltaPosition = 5;
  165.   BackgroundColor = COLORREF(clBlack);
  166.  
  167. implementation
  168.  
  169. uses Math;
  170.  
  171. // Keep track of all shape classes, so choose one at random for creating
  172. // new shapes.
  173. var
  174.   ShapeClassList: array of TShapeClass;
  175.  
  176. // Register new shape classes by adding them to the ShapeClassList.
  177. procedure RegisterShapes(Shapes: array of TShapeClass);
  178. var
  179.   I: Integer;
  180. begin
  181.   SetLength(ShapeClassList, Length(ShapeClassList) + Length(Shapes));
  182.   for I := Low(Shapes) to High(Shapes) do
  183.     ShapeClassList[Length(ShapeClassList)-Length(Shapes)+I] := Shapes[I];
  184. end;
  185.  
  186. { TShapeList }
  187.  
  188. // Add the THelp message shape.
  189. procedure TShapeList.AddHelp;
  190. begin
  191.   Insert(0, THelp.Create(Point(Width div 2, Height div 2)));
  192. end;
  193.  
  194. // Add a random shape on top of other shapes.
  195. procedure TShapeList.AddShape(X, Y: NaturalInt);
  196. var
  197.   ShapeClass: TShapeClass;
  198.   Shape: TShape;
  199. begin
  200.   ShapeClass := AnyShapeClass;
  201.   Shape := ShapeClass.Create(Point(X, Y));
  202.   Add(Shape);
  203. end;
  204.  
  205. // Return a random shape class, chosen from ShapeClassList.
  206. class function TShapeList.AnyShapeClass: TShapeClass;
  207. begin
  208.   if Length(ShapeClassList) = 0 then
  209.     Result := THelp
  210.   else
  211.     Result := ShapeClassList[Random(Length(ShapeClassList))];
  212. end;
  213.  
  214. // Create a new shape list. Remember the screen size.
  215. constructor TShapeList.Create(Width, Height: NaturalInt);
  216. begin
  217.   inherited;
  218.   if Width = 0 then
  219.     fWidth := Screen.Width
  220.   else
  221.     fWidth := Width;
  222.   if Height = 0 then
  223.     fHeight := Screen.Height
  224.   else
  225.     fHeight := Height;
  226. end;
  227.  
  228. // Draw all the shapes on the canvas.
  229. procedure TShapeList.Draw(Canvas: TCanvas);
  230. var
  231.   I: Integer;
  232. begin
  233.   for I := 0 to Count-1 do
  234.     Shapes[I].Draw(Canvas);
  235. end;
  236.  
  237. // Get a shape from the list.
  238. function TShapeList.GetShape(Index: NaturalInt): TShape;
  239. begin
  240.   Result := Items[Index] as TShape;
  241. end;
  242.  
  243. // Create the next generation of shapes. If any shape becomes
  244. // invisible, delete it from the list. Count down so deletion
  245. // does not affect the list iteration.
  246. procedure TShapeList.NextGeneration;
  247. var
  248.   I: Integer;
  249. begin
  250.   for I := Count-1 downto 0 do
  251.   begin
  252.     Shapes[I].NextGeneration(Width, Height);
  253.     if not Shapes[I].IsVisible(Width, Height) then
  254.       Delete(I);
  255.   end;
  256. end;
  257.  
  258. // Map Hue-Saturation-Value to Red-Green-Blue colors.
  259. type
  260.   THue = 0..359;
  261. function HsvToRgb(Hue: THue; Saturation, Value: Byte): TColor;
  262. resourcestring
  263.   sCannotHappen = 'HsvToRgb: Cannot happen, Hue = %d';
  264. var
  265.   P, Q, R: Byte;
  266.   F: Single;
  267. begin
  268.   if Saturation = 0 then
  269.     Result := RGB(Value, Value, Value)
  270.   else
  271.   begin
  272.     F := Frac(Hue / 60);
  273.     P := Value * (255 - Saturation) div 256;
  274.     Q := Round(Value * (255 - Saturation * F)) div 256;
  275.     R := Round(Value * (255 - Saturation * (1 - F))) div 256;
  276.     case Hue div 60 of
  277.     0: Result := RGB(Value, R, P);
  278.     1: Result := RGB(Q, Value, P);
  279.     2: Result := RGB(P, Value, R);
  280.     3: Result := RGB(P, Q, Value);
  281.     4: Result := RGB(R, P, Value);
  282.     5: Result := RGB(Value, P, Q);
  283.     else
  284.       raise Exception.CreateFmt(sCannotHappen, [Hue]);
  285.     end;
  286.   end;
  287. end;
  288.  
  289. // Pick a random color by choosing a random hue and a random
  290. // saturation. Keep the full value of 255, to avoid starting
  291. // with dark colors.
  292. function RandomColor: TColor;
  293. begin
  294.   Result := HsvToRgb(Random(360), 255, 255);
  295. end;
  296.  
  297. { TShape }
  298.  
  299. // Get the shape's bounding box and store it in Rect.
  300. procedure TShape.BoundingBox(var Rect: TRect);
  301. begin
  302.   Rect.Left   := Left;
  303.   Rect.Right  := Right;
  304.   Rect.Top    := Top;
  305.   Rect.Bottom := Bottom;
  306. end;
  307.  
  308. // Change the shape's color by fading slowly to black.
  309. procedure TShape.ChangeColor;
  310. var
  311.   Red, Green, Blue: Integer;
  312. begin
  313.   Red := GetRValue(Color) - Random(DeltaColor);
  314.   if Red < 0 then
  315.     Red := 0;
  316.   Green := GetGValue(Color) - Random(DeltaColor);
  317.   if Green < 0 then
  318.     Green := 0;
  319.   Blue := GetBValue(Color) - Random(DeltaColor);
  320.   if Blue < 0 then
  321.     Blue := 0;
  322.   Color := RGB(Red, Green, Blue);
  323. end;
  324.  
  325. // Increase the shape's size slightly.
  326. procedure TShape.ChangeSize;
  327. begin
  328.   XSize := XSize + Random(DeltaDimension);
  329.   YSize := YSize + Random(DeltaDimension);
  330. end;
  331.  
  332. // Create the shape with a random color.
  333. constructor TShape.Create(Position: TPoint);
  334. begin
  335.   inherited Create;
  336.   fPosition := Position;
  337.   fColor := RandomColor;
  338.   XSize := InitialDimension;
  339.   YSize := InitialDimension;
  340.   GenerateDelta;
  341. end;
  342.  
  343. // Generate a random position delta, which can be positive or negative.
  344. procedure TShape.GenerateDelta;
  345. begin
  346.   fDelta.X := Random(DeltaPosition * 2) - DeltaPosition;
  347.   fDelta.Y := Random(DeltaPosition * 2) - DeltaPosition;
  348. end;
  349.  
  350. // Return the bottom coordinate, assuming a symmetrical shape.
  351. function TShape.GetBottom: Integer;
  352. begin
  353.   Result := Top + YSize;
  354. end;
  355.  
  356. // Return the left coordinate, assuming a symmetrical shape.
  357. function TShape.GetLeft: Integer;
  358. begin
  359.   Result := XPosition - XSize div 2;
  360. end;
  361.  
  362. // Return the right coordinate, assuming a symmetrical shape.
  363. function TShape.GetRight: Integer;
  364. begin
  365.   Result := Left + XSize;
  366. end;
  367.  
  368. // Return the top coordinate, assuming a symmetrical shape.
  369. function TShape.GetTop: Integer;
  370. begin
  371.   Result := YPosition - YSize div 2;
  372. end;
  373.  
  374. // Return True if the shape is invisible: off the screen or completely black.
  375. function TShape.IsVisible(Width, Height: NaturalInt): Boolean;
  376. begin
  377.   Result := (COLORREF(Color) <> BackgroundColor) and
  378.             (Right >= 0) and
  379.             (Bottom >= 0) and
  380.             (Left <= Width-1) and
  381.             (Top <= Height-1);
  382. end;
  383.  
  384. // Move the shape for a generation.
  385. procedure TShape.Move;
  386. begin
  387.   XPosition := XPosition + Delta.X;
  388.   YPosition := YPosition + Delta.Y;
  389. end;
  390.  
  391. // Each generation, move, grow, and recolor the shape.
  392. procedure TShape.NextGeneration(Width, Height: NaturalInt);
  393. begin
  394.   Move;
  395.   ChangeColor;
  396.   ChangeSize;
  397. end;
  398.  
  399. { TEllipse }
  400.  
  401. procedure TEllipse.Draw(Canvas: TCanvas);
  402. begin
  403.   Canvas.Brush.Color := Color;
  404.   Canvas.Pen.Color := Color;
  405.   Canvas.Ellipse(Position.X - XSize div 2, Position.Y - YSize,
  406.                  Position.X + XSize, Position.Y + YSize);
  407. end;
  408.  
  409. { TPolygon }
  410.  
  411. const
  412.   MinVertices = 3;
  413.   MaxVertices = 12;
  414.  
  415. // The bounding box isn't always the smallest bounding box,
  416. // but it's an adequate approximation.
  417. constructor TPolygon.Create(Position: TPoint);
  418. begin
  419.   inherited;
  420.   fNumVertices := Random(MaxVertices - MinVertices + 1) + MinVertices;
  421. end;
  422.  
  423. procedure TPolygon.Draw(Canvas: TCanvas);
  424. var
  425.   I: Integer;
  426.   Pt: TPoint;
  427.   Points: array of TPoint;
  428.   Angle: Single;
  429. begin
  430.   SetLength(Points, NumVertices);
  431.   for I := Low(Points) to High(Points) do
  432.   begin
  433.     Angle := 2*Pi * I / Length(Points);
  434.     Pt.X := Round(Position.X + XSize * Cos(Angle));
  435.     Pt.Y := Round(Position.Y + YSize * Sin(Angle));
  436.     Points[I] := Pt;
  437.   end;
  438.   Canvas.Pen.Color := Color;
  439.   Canvas.Brush.Color := Color;
  440.   Canvas.Polygon(Points);
  441. end;
  442.  
  443.  
  444. { THelp }
  445.  
  446. constructor THelp.Create(Position: TPoint);
  447. begin
  448.   inherited;
  449.   Color := clYellow;
  450.   FontHeight := 48;
  451. end;
  452.  
  453. // Display a help message, telling the user how to exit the program.
  454. procedure THelp.Draw(Canvas: TCanvas);
  455. resourcestring
  456.   HelpMsg = 'Press ESC to end the program';
  457. var
  458.   X, Y: Integer;
  459. begin
  460.   Canvas.Font.Color := Color;
  461.   Canvas.Font.Name := 'Arial';
  462.   Canvas.Font.Height := Round(FontHeight);
  463.   Canvas.Font.Style := [fsBold];
  464.   X := Position.X - Canvas.TextWidth(HelpMsg) div 2;
  465.   Y := Position.Y - Canvas.TextHeight(HelpMsg) div 2;
  466.   Canvas.TextOut(X, Y, HelpMsg);
  467. end;
  468.  
  469. // The help text gets smaller with each generation. When the font size
  470. // reaches zero, the shape becomes invisible. (Note that Windows does not
  471. // allow a font height of zero pixels.)
  472. function THelp.IsVisible(Width, Height: NaturalInt): Boolean;
  473. begin
  474.   Result := inherited IsVisible(Width, Height) and (FontHeight > 1);
  475. end;
  476.  
  477. // The help text gets slowly smaller. Font heights are mapped to integers,
  478. // but using 0.5 each generation slows down the speed at which the text
  479. // becomes invisible.
  480. procedure THelp.ChangeSize;
  481. begin
  482.   FontHeight := FontHeight - 0.5;
  483. end;
  484.  
  485. // The help message doesn't move.
  486. procedure THelp.Move;
  487. begin
  488. end;
  489.  
  490. { TTriangle }
  491.  
  492. procedure TTriangle.AfterConstruction;
  493. begin
  494.   inherited;
  495.   fNumVertices := 3;
  496. end;
  497.  
  498. { TRectangle }
  499.  
  500. // Create the rectangle with a random, but nonzero size.
  501. constructor TRectangle.Create(Position: TPoint);
  502. begin
  503.   inherited;
  504.   YSize := Random(InitialDimension) + 1;
  505.   XSize := Random(InitialDimension) + 1;
  506. end;
  507.  
  508. procedure TRectangle.Draw(Canvas: TCanvas);
  509. var
  510.   Rect: TRect;
  511. begin
  512.   Canvas.Brush.Color := Color;
  513.   BoundingBox(Rect);
  514.   Canvas.FillRect(Rect);
  515. end;
  516.  
  517.  
  518. { TStar }
  519.  
  520. const
  521.   MinStarVertices = 4;
  522.   MaxStarVertices = 8;
  523.  
  524. // For simplicity, a star has more restricted range of number of vertices.
  525. constructor TStar.Create(Position: TPoint);
  526. begin
  527.   inherited;
  528.   fNumVertices := Random(MaxStarVertices - MinStarVertices + 1) + MinStarVertices;
  529. end;
  530.  
  531. procedure TStar.Draw(Canvas: TCanvas);
  532. var
  533.   I: Integer;
  534.   Pt: TPoint;
  535.   Points: array of TPoint;
  536.   Angle: Single;
  537.   Divisor: 1..2;
  538. begin
  539.   SetLength(Points, NumVertices * 2);
  540.   Divisor := 1;
  541.   for I := Low(Points) to High(Points) do
  542.   begin
  543.     Angle := 2*Pi * I / Length(Points);
  544.     Pt.X := Round(Position.X + XSize / Divisor * Cos(Angle));
  545.     Pt.Y := Round(Position.Y + YSize / Divisor * Sin(Angle));
  546.     Points[I] := Pt;
  547.     Divisor := 3 - Divisor;    // Change 1 to 2 and 2 to 1.
  548.   end;
  549.   Canvas.Pen.Color := Color;
  550.   Canvas.Brush.Color := Color;
  551.   Canvas.Polygon(Points);
  552. end;
  553.  
  554. initialization
  555.   // To create lots of polygons, register TPolygon more than once.
  556.   RegisterShapes([TEllipse, TTriangle, TRectangle, TStar,
  557.                   TPolygon, TPolygon, TPolygon]);
  558. end.
  559.